<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * 
 *		http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @package mysql
 */

/**
 * This is the central class in the mysql package. All database operations 
 * are done through this class.
 * 
 * The main mysql methods are:
 * 	<ul>
 * 		<li>{@link connect()}</li>
 * 		<li>{@link query()}</li>
 * 		<li>{@link prepare()}</li>
 * 		<li>{@link execute()}</li>
 * 		<li>{@link results()}</li>
 * 		<li>{@link fatal()}</li>
 * 	</ul>
 * 
 * @package    mysql
 * @license	   http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */
class MySQL {

	/**
	 * Set up the database connection
	 */
	public function __construct($host, $user, $pass, $db, $charset = 'utf8', $port = NULL, $persistant = true){
		$this->connection = $this->connect($host, $user, $pass, $db, $port, $charset, $persistant);
	}

	/**
	 * Connect to the database, with or without a persistant connection
	 * @param  String  $host       Mysql server hostname
	 * @param  String  $user       Mysql username
	 * @param  String  $pass       Mysql password
	 * @param  String  $db         Database to use
	 * @param  String  $charset    Connection charset
	 * @param  boolean $persistant Create a persistant connection
	 * @return Object              Mysqli
	 */
	private function connect($host, $user, $pass, $db, $port = NULL, $charset = 'utf8', $persistant = true){
		$host = $persistant === true ? 'p:'.$host : $host;
		if($port == NULL)
            $port = ini_get('mysqli.default_port');

		$mysqli = new mysqli($host, $user, $pass, $db, $port);

		if($mysqli->connect_error)
			throw new Exception('Connection Error: '.$mysqli->connect_error);
		$mysqli->set_charset($charset);
		return $mysqli;
	}

	/**
	 * Execute an SQL statement for execution.
	 * @param  String $sql An SQL query
	 * @return Object      $this
	 */
	public function query($sql){
		$this->num_rows = 0;
		$this->affected_rows = -1;

		if(is_object($this->connection)){
			$stmt = $this->connection->query($sql);
			# Affected rows has to go here for query :o
			$this->affected_rows = $this->connection->affected_rows;
			$this->stmt = $stmt;
			return $this;
		}
		else {
			throw new Exception;
		}
	}

	/**
	 * Prepare an SQL statement
	 * @param  String $sql An SQL query
	 * @return Object      $this
	 */
	public function prepare($sql){
		$this->num_rows = 0;
		$this->affected_rows = -1;

		if(is_object($this->connection)){
			# Ready the stmt
			$this->stmt = $this->connection->prepare($sql);
			return $this;
		}
		else {
			throw new Exception;
		}
	}

	/**
	 * Escapes the arguments passed in and executes a prepared Query.
	 * @param Mixed $var   The value to be bound to the first SQL ?
	 * @param Mixed $...   Each subsequent value to be bound to ?
	 * @return Object      $this
	 */
	public function execute(){
		if(is_object($this->connection) && is_object($this->stmt)){
			# Ready the params
			if(count($args = func_get_args()) > 0){
				$types = array();
				$params = array();

				foreach($args as $arg){
					$types[] = is_int($arg) ? 'i' : (is_float($arg) ? 'd' : 's');
					$params[] = $this->connection->real_escape_string($arg);
				}

				# Stick the types at the start of the params
				array_unshift($params, implode($types));

				# Call bind_param (avoiding the pass_by_reference crap)
				call_user_func_array(
					array($this->stmt, 'bind_param'),
					$this->_pass_by_reference($params)
				);
			}

			if($this->stmt->execute()){
				# Affected rows to be run after execute for prepares
				$this->affected_rows = $this->stmt->affected_rows;
				return $this;
			}
			else {
				throw new Exception;
			}
		}
		else {
			throw new Exception;
		}
	}

	/**
	 * Fetch all results as an array, the type of array depend on the $method passed through.
	 * @param  boolean $close_stmt Optional perameter to indicate if the statement should be destroyed after execution.
	 * @return Array              Array of database results
	 */
	public function results($close_stmt = false){
		if( is_object( $this->stmt ) ) {
			$stmt_type = get_class($this->stmt);
			# Grab the result prepare() & query()
			switch($stmt_type){
				case 'mysqli_stmt':
					$meta = $this->stmt->result_metadata();
				    while ( $field = $meta->fetch_field() ) { 
				        $params[] = &$row[$field->name]; 
				    }
				    call_user_func_array(array($this->stmt, 'bind_result'), $params); 
				    $result = array();
				    while ($this->stmt->fetch()) { 
				        foreach($row as $key => $val) { 
				            $c[$key] = $val; 
				        } 
				        $result[] = $c;
				    } 
				    $this->num_rows = count($result);
				    if ($close_stmt) $this->stmt->close(); 
					break;

				case 'mysqli_result':
					$result = array();
					while($row = $this->stmt->fetch_assoc()){
						$result[] = $row;
					}
					$this->num_rows = $this->stmt->num_rows;
					if ($close_stmt) $this->stmt->free(); 
					break;

				default:
					throw new Exception;
			}
		    return $result;
		}
		else {
			throw new Exception;
		}
	}

	/**
	 * Turns off auto-committing database modifications, starting a new transaction.
	 * @return bool Dependant on the how successful the autocommit() call was
	 */
	public function start_transaction(){
		if(is_object($this->connection)){
			return $this->connection->autocommit(false);
		}
	}

	/**
	 * Commits the current transaction and turns auto-committing database modifications on, ending transactions.
	 * @return bool Dependant on the how successful the autocommit() call was
	 */
	public function commit(){
		if(is_object($this->connection)){
			# Commit!
			if($this->connection->commit()){
				return $this->connection->autocommit(true);
			}
			else {
				$this->connection->autocommit(true);
				throw new Exception;
			}
		}
	}

	/**
	 * Rolls back current transaction and turns auto-committing database modifications on, ending transactions.
	 * @return bool Dependant on the how successful the autocommit() call was
	 */
	public function rollback(){
		if(is_object($this->connection)){
			# Commit!
			if($this->connection->rollback()){
				return $this->connection->autocommit(true);
			}
			else {
				$this->connection->autocommit(true);
				throw new Exception;
			}
		}
	}

	/**
	 * Return the number of rows in statements result set.
	 * @return integer The number of rows
	 */
	public function num_rows(){
		return $this->num_rows;
	}

	/**
	 * Gets the number of affected rows in a previous MySQL operation.
	 * @return integer The affected rows
	 */
	public function affected_rows(){
		return $this->affected_rows;
	}

	/**
	 * Returns the auto generated id used in the last query.
	 * @return integer The last auto generated id
	 */
	public function insert_id(){
		if(is_object($this->connection)){
			return $this->connection->insert_id;
		}
	}

	/**
	 * Fixes the call_user_func_array & bind_param pass by reference crap.
	 * @param  array $arr The array to be referenced
	 * @return array      A referenced array
	 */
	private function _pass_by_reference(&$arr){
		$refs = array();
		foreach($arr as $key => $value){
			$refs[$key] = &$arr[$key];
		}
		return $refs;
	}
}